//
//  ITDownloader.m
//  InduceTool
//
//  Created by 魏延龙 on 2020/9/9.
//  Copyright © 2020 魏延龙. All rights reserved.
//

#import "ITDownloader.h"
#import "NSURLSession+CorrectedResumeData.h"

NSString *const ITDownloaderErrorDomain = @"com.thibaultcha.itdownload";

@interface ITDownloader ()<NSURLSessionDownloadDelegate>

@property (nonatomic, copy) NSURL *downloadURL;
@property (nonatomic, copy) NSString *toPath;
@property (strong, nonatomic) NSData *resumeData;

@property (strong, nonatomic) NSURLSessionDownloadTask *downloadTask;
@property (strong, nonatomic) NSURLSession *backgroundSession;

// Blocks
@property (nonatomic, copy) void (^ progressBlock)(uint64_t receivedLength, uint64_t totalLength, NSInteger remainingTime, float progress);
@property (nonatomic, copy) void (^ errorBlock)(NSError *error);
@property (nonatomic, copy) void (^ completeBlock)(BOOL downloadFinished, NSString *pathToFile);

@end

@implementation ITDownloader

- (NSString *)toPath {
    if (!_toPath) {
        NSString *dir = [[NSString stringWithString:NSTemporaryDirectory()] stringByAppendingPathComponent:@"itdownload"];
        NSFileManager *fileManager = [NSFileManager defaultManager];
        BOOL isDir = NO;
        BOOL isDirExist = [fileManager fileExistsAtPath:dir isDirectory:&isDir];
        if (isDirExist && isDir) {
            return dir;
        } else {
            BOOL result = [fileManager createDirectoryAtPath:dir withIntermediateDirectories:YES attributes:nil error:nil];
            if (result) {
                return dir;
            }
            return nil;
        }
    }
    return _toPath;
}

- (instancetype)initWithUrl:(NSString *)url
{
    self = [super init];
    if (self) {
        self.downloadURL = [NSURL URLWithString:url];
        self.backgroundSession = [self backgroundURLSession];
    }
    return self;
}

- (instancetype)initWithUrl:(NSString *)url toPath:(NSString *)path progress:(nonnull void (^)(uint64_t, uint64_t, NSInteger, float))progressBlock error:(nonnull void (^)(NSError *_Nonnull))errorBlock complete:(nonnull void (^)(BOOL, NSString *_Nonnull))completeBlock
{
    self = [super init];
    if (self) {
        self.toPath = path;
        self.downloadURL = [NSURL URLWithString:url];
        self.backgroundSession = [self backgroundURLSession];

        self.progressBlock = progressBlock;
        self.errorBlock = errorBlock;
        self.completeBlock = completeBlock;
    }
    return self;
}

#pragma mark - backgroundURLSession
- (NSURLSession *)backgroundURLSession {
    static NSURLSession *session = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSString *identifier = @"com.wyl.app.BackgroundSession";
        NSURLSessionConfiguration *sessionConfig = nil;
        sessionConfig = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:identifier];
        session = [NSURLSession sessionWithConfiguration:sessionConfig
                                                delegate:self
                                           delegateQueue:[NSOperationQueue mainQueue]];
    });

    return session;
}

#pragma mark - Public Mehtod

- (void)suspend {
    [self.downloadTask suspend];
}

- (void)cancel {
    [self.downloadTask cancel];
    [self willChangeValueForKey:@"isCancelled"];
    [self finishOperationWithState:ITDownloaderCancelled];
    [self didChangeValueForKey:@"isCancelled"];
}

#pragma mark - NSOperation Override

- (void)start
{
    @synchronized (self) {
        if ([self isCancelled]) {
            [self notifyFromCompletionWithError:[NSError errorWithDomain:ITDownloaderErrorDomain code:ITDownloaderCancelled userInfo:@{ NSLocalizedDescriptionKey: @"Operation cancelled by user before sending the request" }] pathToFile:nil];
            return;
        }

        // Initialization of everything we'll need to download the file

        [self.backgroundSession getTasksWithCompletionHandler:^(NSArray<NSURLSessionDataTask *> *_Nonnull dataTasks, NSArray<NSURLSessionUploadTask *> *_Nonnull uploadTasks, NSArray<NSURLSessionDownloadTask *> *_Nonnull downloadTasks) {
            NSLog(@"[downloadTask] backgroundSession %@", downloadTasks.description);
            __block BOOL downloading = false;
            [downloadTasks enumerateObjectsUsingBlock:^(NSURLSessionDownloadTask *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) {
                if ([obj.originalRequest.URL isEqual:self.downloadURL] && obj.state == NSURLSessionTaskStateRunning) {
                    [self willChangeValueForKey:@"isExecuting"];
                    self.state = ITDownloaderDownloading;
                    [self didChangeValueForKey:@"isExecuting"];
                    self.downloadTask = obj;
                    downloading = YES;
                }
            }];

            if (!downloading) {
                NSURLRequest *request = [NSURLRequest requestWithURL:self.downloadURL];
                self.downloadTask = [self.backgroundSession downloadTaskWithRequest:request];
                if (self.downloadTask && ![self isCancelled]) {
                    [self willChangeValueForKey:@"isExecuting"];
                    self.state = ITDownloaderDownloading;
                    [self didChangeValueForKey:@"isExecuting"];

                    // Start the download
                    [self.downloadTask resume];
                } else {
                    [self notifyFromCompletionWithError:[NSError errorWithDomain:ITDownloaderErrorDomain code:ITDownloaderFailed userInfo:@{ NSLocalizedDescriptionKey: @"Task can't be initialized" }] pathToFile:nil];
                }
            }
        }];
    }
}

- (BOOL)isExecuting
{
    return self.state == ITDownloaderDownloading;
}

- (BOOL)isCancelled
{
    return self.state == ITDownloaderCancelled;
}

- (BOOL)isFinished
{
    return self.state == ITDownloaderCancelled || self.state == ITDownloaderDone || self.state == ITDownloaderFailed;
}

- (void)notifyFromCompletionWithError:(NSError *)error pathToFile:(NSString *)pathToFile
{
    if (self.errorBlock) {
        self.errorBlock(error);
    }
}

#pragma mark - Internal Methods
- (void)finishOperationWithState:(ITDownloaderState)state
{
    // Cancel the connection in case cancel was called directly
    [self.downloadTask cancel];

    // Let's finish the operation once and for all
    if ([self isExecuting]) {
        [self willChangeValueForKey:@"isFinished"];
        [self willChangeValueForKey:@"isExecuting"];
        self.state = state;
        [self didChangeValueForKey:@"isExecuting"];
        [self didChangeValueForKey:@"isFinished"];
    } else {
        [self willChangeValueForKey:@"isExecuting"];
        self.state = state;
        [self didChangeValueForKey:@"isExecuting"];
    }

    [self reset];
}

- (void)reset {
    @synchronized (self) {
        self.downloadTask = nil;
    }
}

#pragma mark - NSURLSessionDownloadDelegate
- (void)           URLSession:(NSURLSession *)session
                 downloadTask:(NSURLSessionDownloadTask *)downloadTask
    didFinishDownloadingToURL:(NSURL *)location {
    NSLog(@"[downloadTask] downloadTask:%lu didFinishDownloadingToURL:%@", (unsigned long)downloadTask.taskIdentifier, location);
    if (!self.toPath) {
        [self notifyFromCompletionWithError:[NSError errorWithDomain:ITDownloaderErrorDomain code:ITDownloaderFailed userInfo:@{ NSLocalizedDescriptionKey: @"Dir can't be created" }] pathToFile:nil];
    } else {
        // 用 NSFileManager 将文件复制到应用的存储中
        NSString *fileName = self.downloadURL.lastPathComponent;
        NSString *locationString = [location path];
        NSString *finalLocation = [self.toPath stringByAppendingPathComponent:fileName];
        NSError *error;
        [[NSFileManager defaultManager] moveItemAtPath:locationString toPath:finalLocation error:&error];
        if (self.completeBlock) {
            self.completeBlock(YES, finalLocation);
        }
    }

    // 通知 UI 刷新
}

- (void)    URLSession:(NSURLSession *)session
          downloadTask:(NSURLSessionDownloadTask *)downloadTask
     didResumeAtOffset:(int64_t)fileOffset
    expectedTotalBytes:(int64_t)expectedTotalBytes {
    NSLog(@"[downloadTask] fileOffset:%lld expectedTotalBytes:%lld", fileOffset, expectedTotalBytes);
}

- (void)           URLSession:(NSURLSession *)session
                 downloadTask:(NSURLSessionDownloadTask *)downloadTask
                 didWriteData:(int64_t)bytesWritten
            totalBytesWritten:(int64_t)totalBytesWritten
    totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
    NSLog(@"[downloadTask] downloadTask:%lu percent:%.2f%%", (unsigned long)downloadTask.taskIdentifier, (CGFloat)totalBytesWritten / totalBytesExpectedToWrite * 100);
    if (self.progressBlock) {
        self.progressBlock(bytesWritten, totalBytesWritten, 0, (float)totalBytesWritten / totalBytesExpectedToWrite * 100);
    }
}

- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
    NSLog(@"[downloadTask] Background URL session %@ finished events.\n", session);
}

/*
 * 该方法下载成功和失败都会回调，只是失败的是error是有值的，
 * 在下载失败时，error的userinfo属性可以通过NSURLSessionDownloadTaskResumeData
 * 这个key来取到resumeData(和上面的resumeData是一样的)，再通过resumeData恢复下载
 */
- (void)      URLSession:(NSURLSession *)session
                    task:(NSURLSessionTask *)task
    didCompleteWithError:(NSError *)error {
    if (error) {
        // check if resume data are available
        if ([error.userInfo objectForKey:NSURLSessionDownloadTaskResumeData]) {
            NSData *resumeData = [error.userInfo objectForKey:NSURLSessionDownloadTaskResumeData];
            //通过之前保存的resumeData，获取断点的NSURLSessionTask，调用resume恢复下载
            self.resumeData = resumeData;
        }
        [self notifyFromCompletionWithError:error pathToFile:nil];
    }
}

@end
